using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Text;
using System.Xml.Serialization;
using SubSonic.Utilities;

namespace SubSonic.Enterprise
{
    public class GenericRecord<T> : IDataErrorInfo where T : GenericRecord<T>, new() 
    {
        #region State Properties

        public virtual void Initialize()
        {
        }

        private bool _isLoaded = false;
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsLoaded
        {
            get { return _isLoaded; }
            protected internal set { _isLoaded = value; _isNew = !value; }
        }

        internal bool _isNew = true; // ML Fix: VB is case insensitive; field name must not be the same as the property name.

        /// <summary>
        /// True if data in the object needs to be saved
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsNew
        {
            get { return _isNew; }
            protected internal set { _isNew = value; _isLoaded = !value; }
        }


        /// <summary>
        /// Automatically called upon object creation. Sets IsNew to true;
        /// </summary>
        public void MarkNew()
        {
            _isNew = true;
        }


        protected bool _isDirty = false; // ML Fix: VB is case insensitive; field name must not be the same as the property name.

        /// <summary>
        /// True if data in the object has been changed and differs from DB.
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsDirty
        {
            get
            {
                return HasChanges();
                //return _isDirty;
            }
            //protected set { _isDirty = value; }
        }

        /// <summary>
        /// Called after any property is set. Sets IsDirty to True.
        /// </summary>
        //protected void MarkDirty()
        //{
        //    _isDirty = true;
        //}

        public void MarkOld()
        {
            _isNew = false;
        }

        private string tableName;

        /// <summary>
        /// Name of the table
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string TableName
        {
            get { return tableName; }
            protected set { tableName = value; }
        }

        #endregion

        #region Object Overrides

        public string Inspect()
        {
            return Inspect(true);
        }

        public string Inspect(bool useHtml)
        {
            StringBuilder sb = new StringBuilder();
            string sOut;
            if (useHtml)
            {
                sb.Append("<table><tr><td colspan=2><h3>" + BaseSchema.Name + " Inspection</h3></td></tr>");

                foreach (TableSchema.TableColumn col in BaseSchema.Columns)
                {
                    sb.Append("<tr><td><span style=\"font-weight:bold\">" + col.ColumnName + "</span></td><td>" + GetColumnValue<object>(col.ColumnName) + "</td></tr>");
                }
                sb.Append("</table>");
                sOut = sb.ToString();
            }
            else
            {
                sb.AppendLine("#################" + BaseSchema.Name + " Inspection ####################");

                foreach (TableSchema.TableColumn col in BaseSchema.Columns)
                {
                    sb.AppendLine(col.ColumnName + ": " + GetColumnValue<object>(col.ColumnName));
                }
                sb.AppendLine("#############################################################################");
                sOut = sb.ToString();
            }
            return sOut;
        }


        public override string ToString()
        {
            //return the value in the second column
            //as by our convention, this is the "descriptor" column.
            string result = "";
            if (columnSettings != null)
            {
                if (columnSettings.Count > 1)
                {
                    result = columnSettings[1].CurrentValue.ToString();
                }
                else if (columnSettings.Count == 1)
                {
                    result = columnSettings[0].CurrentValue.ToString();
                }
            }
            return result;
        }

        #endregion

        #region DB Properties/Methods

        /// <summary>
        /// Returns a default setting per data type
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        protected static object GetDefaultSetting(TableSchema.TableColumn column)
        {
            return Utility.GetDefaultSetting(column);
        }

        public DbType GetDBType(string columnName)
        {
            TableSchema.TableColumn col = BaseSchema.GetColumn(columnName);
            return col.DataType;
        }

        protected static TableSchema.Table table;

        /// <summary>
        /// The base static class that holds all schema info for the table.
        /// The class must be instanced at least once to populate this table.
        /// </summary>
        protected static TableSchema.Table BaseSchema
        {
            get
            {
                if (table == null)
                //if (!IsSchemaInitialized)
                {
                    new T();
                }
                return table;
            }
            set { table = value; }
        }

        protected static bool IsSchemaInitialized
        {
            get { return (table != null && table.Columns != null && table.Columns.Count > 0); }
        }

        /// <summary>
        /// Sets the Primary Key of the object
        /// </summary>
        /// <param name="oValue"></param>
        protected void SetPrimaryKey(object oValue)
        {
            columnSettings.SetValue(BaseSchema.PrimaryKey.ColumnName, oValue);
        }

        /// <summary>
        /// Returns the current value of the primary key
        /// </summary>
        /// <returns></returns>
        public object GetPrimaryKeyValue()
        {
            //return columnSettings.GetValue(table.PrimaryKey.ColumnName).ToString();
            return columnSettings.GetValue<object>(BaseSchema.PrimaryKey.ColumnName);
        }

        /// <summary>
        /// The column settings hold the current values of the object in a collection
        /// so that reflection is not needed in the base class to fill out the commands
        /// </summary>
        private TableSchema.TableColumnSettingCollection columnSettings = null;
        
        public void SetColumnValue(string columnName, object oValue)
        {
            if (columnSettings == null)
            {
                columnSettings = new TableSchema.TableColumnSettingCollection();
            }

            columnSettings.SetValue(columnName, oValue);

        }

        /// <summary>
        /// Returns the current value of a column.
        /// </summary>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public CT GetColumnValue<CT>(string columnName)
        {
            CT oOut = default(CT);

            if (columnSettings != null)
            {
                oOut = columnSettings.GetValue<CT>(columnName);
            }

            return oOut;
        }

        /* jeff_huntsman: added to support getting Original Values for columns */
        public CT GetOriginalColumnValue<CT>(string columnName)
        {
            CT oOut = default(CT);

            if (columnSettings != null)
            {
                oOut = columnSettings.GetOriginalValue<CT>(columnName);
            }

            return oOut;
        }

        /* jeff_huntsman: added to support change checking against Original Values */
        public bool HasChanges()
        {
            foreach (TableSchema.TableColumn column in BaseSchema.Columns)
            {
                if (this.HasChanges(column.ColumnName))
                    return true;
            }
            return false;
        }

        public bool HasChanges(string columnName)
        {
            if (this.IsNew)
                return true;

            if (columnSettings != null)
            {
                return columnSettings.GetOriginalValue(columnName).Equals(columnSettings.GetValue(columnName));
            }
            return false;
        }


        private string _providerName;

        [HiddenForDataBinding(true)]
        public string ProviderName
        {
            get { return _providerName; }
            protected set { _providerName = value; }
        }

        public TableSchema.Table GetSchema()
        {
            return BaseSchema;
        }

        #endregion

        #region "Loaders"
        /// <summary>
        /// Loads the object with the current reader's values. Assumes the reader is already moved to 
        /// first position in recordset (aka has been "Read()")
        /// </summary>
        /// <param name="tbl"></param>
        public void Load(DataTable tbl)
        {
            if (tbl.Rows.Count > 0)
            {
                DataRow dr = tbl.Rows[0];
                Load(dr);
                IsLoaded = true;
                IsNew = false;
            }
        }

        /// <summary>
        /// Loads the object with the current DataRow's values. 
        /// </summary>
        /// <param name="dr"></param>
        public void Load(DataRow dr)
        {
            // jeff_huntsman: we need to turn IsLoaded off while we load
            // for the OriginalValues to be setup again
            IsLoaded = false;
            foreach (TableSchema.TableColumn col in BaseSchema.Columns)
            {
                try
                {
                    SetColumnValue(col.ColumnName, dr[col.ColumnName]);                    
                }
                catch (Exception x)
                {
                    //this will happen only if there's a reader error.
                    throw new Exception("Unable to set column value for " + col.ColumnName + "; " + x.Message);
                }
            }
            IsNew = false;
            IsLoaded = true;
        }
        #endregion

        #region "Clone and Copy Methods"
        /// <summary>
        /// Copies the current instance to a new instance
        /// </summary>
        /// <returns>New instance of current object</returns>
        public T Clone()
        {
            //copy this instance to a new instance
            T thisInstance = new T();

            this.CopyTo(thisInstance);
            
            return thisInstance;
        }

        /// <summary>
        /// Copies current instance to passed in instance
        /// </summary>
        /// <param name="copyInstance"></param>
        public void CopyTo(T copyInstance)
        {
            if (copyInstance == null)
            {
                copyInstance = new T();
            }
            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                copyInstance.SetColumnValue(setting.ColumnName, setting.CurrentValue);                
            }

            // clone record state too            
            copyInstance.IsNew = this.IsNew;
            copyInstance.IsLoaded = this.IsLoaded;
        }

        /// <summary>
        /// Adds a new row to a Datatable with this record
        /// You must be sure the column names are the same
        /// </summary>
        /// <param name="dataTable"></param>
        public void CopyTo(DataTable dataTable)
        {
            DataRow newRow = dataTable.NewRow();
            
            CopyTo(newRow);                
            
            dataTable.Rows.Add(newRow);
        }

        public void CopyTo(DataRow dataRow)
        {
            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                try
                {
                    dataRow[setting.ColumnName] = setting.OriginalValue;                    
                    
                }
                catch
                {
                    //swallow this - this is a forgiving procedure :)
                }
            }

            dataRow.AcceptChanges();

            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                try
                {
                    dataRow[setting.ColumnName] = setting.CurrentValue;
                }
                catch
                {
                    //swallow this - this is a forgiving procedure :)
                }
            }
            
        }

        /// <summary>
        /// Copies a record from a DataTable to this instance. Column names must match.
        /// </summary>
        /// <param name="row"></param>
        public void CopyFrom(DataRow row)
        {
            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                try
                {
                    setting.CurrentValue = row[setting.ColumnName];
                    setting.OriginalValue = row[setting.ColumnName, DataRowVersion.Original];
                }
                catch
                {
                    //swallow this - this is a forgiving procedure :)
                }
            }            
        }

        /// <summary>
        /// Copies the passed-in instance settings to this instance
        /// </summary>
        /// <param name="copyInstance"></param>
        public void CopyFrom(T copyInstance)
        {
            if (copyInstance != null)
            {
                foreach (TableSchema.TableColumnSetting setting in copyInstance.GetColumnSettings())
                {
                    SetColumnValue(setting.ColumnName, setting.CurrentValue);                    
                }
            }
            else
            {
                throw new Exception("Copy instance is null");
            }
        }
        #endregion

        #region Serializers

        public string ToXML()
        {
            Type type = typeof(T);
            XmlSerializer ser = new XmlSerializer(type);
            using (System.IO.MemoryStream stm = new System.IO.MemoryStream())
            {
                //serialize to a memory stream
                ser.Serialize(stm, this);

                //reset to beginning so we can read it.  
                stm.Position = 0;
                //Convert a string. 
                using (System.IO.StreamReader stmReader = new System.IO.StreamReader(stm))
                {
                    string xmlData = stmReader.ReadToEnd();
                    return xmlData;
                }
            }
        }

        /// <summary>
        /// Returns an object based on the passed-in XML.
        /// </summary>
        /// <param name="xml"></param>
        /// <returns></returns>
        public object NewFromXML(string xml)
        {
            object oOut = null;
            Type type = typeof(T);
            //hydrate based on private string var
            if (xml.Length > 0)
            {
                XmlSerializer serializer = new XmlSerializer(type);
                StringBuilder sb = new StringBuilder();
                sb.Append(xml);
                System.IO.StringReader sReader = new System.IO.StringReader(xml);
                oOut = serializer.Deserialize(sReader);
                //sb = null;
                sReader.Close();
            }

            return oOut;
        }

        #endregion

        #region "Utility"
        public TableSchema.TableColumnSettingCollection GetColumnSettings()
        {
            return columnSettings;
        }
        #endregion

        #region WebUI Helper

        private string nullExceptionMessage = "{0} requires a value";
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string NullExceptionMessage
        {
            get { return nullExceptionMessage; }
            protected set { nullExceptionMessage = value; }
        }

        private string invalidTypeExceptionMessage = "{0} is not a valid {1}";
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string InvalidTypeExceptionMessage
        {
            get { return invalidTypeExceptionMessage; }
            protected set { invalidTypeExceptionMessage = value; }
        }

        private string lengthExceptionMessage = "{0} exceeds the maximum length of {1}";
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string LengthExceptionMessage
        {
            get { return lengthExceptionMessage; }
            protected set { lengthExceptionMessage = value; }
        }

        private System.Collections.Generic.List<string> errorList = new System.Collections.Generic.List<string>();
        [HiddenForDataBinding(true)]
        protected System.Collections.Generic.List<string> Errors
        {
            get { return errorList; }
        }

        public bool HasErrors()
        {
            return errorList.Count > 0;
        }

        /// <summary>
        /// Loops the underlying settings collection to validate type, nullability, and length
        /// </summary>
        public void ValidateColumnSettings()
        {
            //loop the current settings
            //make sure they are valid for their type
            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                Utility.WriteTrace("Validating " + setting.ColumnName);
                object settingValue = setting.CurrentValue;
                TableSchema.TableColumn col = table.GetColumn(setting.ColumnName);
                string formattedName = Utility.ParseCamelToProper(col.ColumnName);
                Type t = col.GetPropertyType();

                /* jeff_huntsman changed: we really do not need to validate any
                 * non writable columns because they will not be written to the
                 * database anyway.
                 */
                if (!col.IsReadOnly)
                {
                    //Convert the existing value to the type for this column
                    //if there's an error, report it.
                    //OK to bypass if the column is nullable and this setting is null
                    //just check for now if the value isn't null - it will be checked
                    //later for nullability

                    if (!col.IsNullable && settingValue != null && settingValue != DBNull.Value)
                    {
                        try
                        {
                            if (col.DataType != DbType.Guid)
                            {
                                Convert.ChangeType(settingValue, t);
                            }
                        }
                        catch
                        {
                            //there's a conversion problem here
                            //add it to the Exception List<>
                            if (col.IsNumeric)
                            {
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "number"));
                            }
                            else if (col.IsDateTime)
                            {
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "date"));
                            }
                            else
                            {
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "value"));
                            }
                        }
                    }
                    //now make sure that this column's null settings match with what's in the setting
                    Utility.WriteTrace("Testing nullability of " + setting.ColumnName);
                    if (!col.IsNullable && (settingValue == null || settingValue == DBNull.Value))
                    {
                        Utility.WriteTrace("Null Error Caught " + setting.ColumnName);
                        errorList.Add(String.Format(NullExceptionMessage, formattedName));
                    }


                    //finally, check the length
                    Utility.WriteTrace("Testing Max Length of " + setting.ColumnName);
                    if ((settingValue != null && settingValue != DBNull.Value) && col.MaxLength > 0)
                    {
                        if (settingValue.ToString().Length > col.MaxLength)
                        {
                            if (settingValue.ToString().Length > col.MaxLength)
                            {
                                Utility.WriteTrace("Max Length Exceeded " + col.ColumnName + " can't exceed " + col.MaxLength + "; current value is set to " + settingValue.ToString().Length);
                                errorList.Add(String.Format(LengthExceptionMessage, formattedName, col.MaxLength));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Loads your object from a form postback
        /// </summary>
        public void LoadFromPost()
        {
            LoadFromPost(true);
        }

        /// <summary>
        /// Loads your object from a form postback
        /// </summary>
        /// <param name="validatePost">Set this to false to skip validation</param>
        public void LoadFromPost(bool validatePost)
        {
            if (System.Web.HttpContext.Current != null)
            {
                //use Request.form, since the ControlCollection can return weird results based on 
                //the container structure.
                System.Collections.Specialized.NameValueCollection formPost = System.Web.HttpContext.Current.Request.Form;
                TableSchema.TableColumnSettingCollection settings = GetColumnSettings();

                if (formPost != null && settings != null)
                {
                    foreach (string s in formPost.AllKeys)
                    {
                        Utility.WriteTrace("Looking at form field " + s);

                        foreach (TableSchema.TableColumnSetting setting in settings)
                        {
                            if (s.ToLower().EndsWith("_" + setting.ColumnName.ToLower()) || s.ToLower().EndsWith("$" + setting.ColumnName.ToLower()) || s.ToLower().Equals(setting.ColumnName.ToLower()))
                            {
                                setting.CurrentValue = formPost[s];
                                Utility.WriteTrace("Matched " + s + " to " + setting.ColumnName);
                            }
                        }
                    }
                }
                //validate the settings, since we're setting the object values here, not
                //using the accessors as we should be.
                if (validatePost)
                {
                    ValidateColumnSettings();

                    if (errorList.Count > 0)
                    {
                        //format this for the web
                        if (System.Web.HttpContext.Current != null)
                        {
                            //decorate the output
                            string errorReport = "<b>Validation Error:<b><ul>";
                            foreach (string s in errorList)
                            {
                                errorReport += "<li><i>" + s + "</i></li>";
                            }
                            errorReport += "</ul>";
                            throw new Exception(errorReport);
                        }
                        else
                        {
                            throw new Exception("Validation error - catch this and check the ExceptionList property to review the exceptions. You can change the output message as needed by accessing the ExceptionMessage properties of this object");
                        }
                    }
                }
            }
        }

        #endregion

        #region IDataErrorInfo Members

        string IDataErrorInfo.Error
        {
            get
            {
                System.Text.StringBuilder sb = new StringBuilder();
                foreach (string err in this.Errors)
                {
                    sb.AppendLine(err);
                }
                return sb.ToString();
            }
        }

        string IDataErrorInfo.this[string columnName]
        {
            get
            {
                // TODO
                throw new NotImplementedException();
            }
        }

        #endregion
    }
}
